home *** CD-ROM | disk | FTP | other *** search
/ Revista do CD-ROM 151 / cd-rom 151.iso / internet / firefox / Firefox Setup 3.0 Beta 1.exe / nonlocalized / modules / Microformats.js < prev    next >
Encoding:
Text File  |  2007-11-09  |  56.4 KB  |  1,659 lines

  1. var EXPORTED_SYMBOLS = ["Microformats", "adr", "tag", "hCard", "hCalendar", "geo"];
  2.  
  3. var Microformats = {
  4.   version: 0.8,
  5.   /* When a microformat is added, the name is placed in this list */
  6.   list: [],
  7.   /* Custom iterator so that microformats can be enumerated as */
  8.   /* for (i in Microformats) */
  9.   __iterator__: function () {
  10.     for (let i=0; i < this.list.length; i++) {
  11.       yield this.list[i];
  12.     }
  13.   },
  14.   /**
  15.    * Retrieves microformats objects of the given type from a document
  16.    * 
  17.    * @param  name          The name of the microformat (required)
  18.    * @param  rootElement   The DOM element at which to start searching (required)
  19.    * @param  recurseFrames Whether or not to search child frames for microformats (optional - defaults to true)
  20.    * @param  targetArray  An array of microformat objects to which is added the results (optional)
  21.    * @return A new array of microformat objects or the passed in microformat 
  22.    *         object array with the new objects added
  23.    */
  24.   get: function(name, rootElement, recurseFrames, targetArray) {
  25.     if (!Microformats[name]) {
  26.       return;
  27.     }
  28.     targetArray = targetArray || [];
  29.  
  30.     rootElement = rootElement || content.document;
  31.  
  32.     /* If recurseFrames is undefined or true, look through all child frames for microformats */
  33.     if ((recurseFrames == undefined) || (recurseFrames == true)) {
  34.       if (rootElement.defaultView && rootElement.defaultView.frames.length > 0) {
  35.         for (let i=0; i < rootElement.defaultView.frames.length; i++) {
  36.           Microformats.get(name, rootElement.defaultView.frames[i].document, recurseFrames, targetArray);
  37.         }
  38.       }
  39.     }
  40.  
  41.     /* Get the microformat nodes for the document */
  42.     var microformatNodes = [];
  43.     if (Microformats[name].className) {
  44.       microformatNodes = Microformats.getElementsByClassName(rootElement,
  45.                                         Microformats[name].className);
  46.       /* alternateClassName is for cases where a parent microformat is inferred by the children */
  47.       /* If we find alternateClassName, the entire document becomes the microformat */
  48.       if ((microformatNodes.length == 0) && Microformats[name].alternateClassName) {
  49.         var altClass = Microformats.getElementsByClassName(rootElement, Microformats[name].alternateClassName);
  50.         if (altClass.length > 0) {
  51.           microformatNodes.push(rootElement); 
  52.         }
  53.       }
  54.     } else if (Microformats[name].attributeValues) {
  55.       microformatNodes =
  56.         Microformats.getElementsByAttribute(rootElement,
  57.                                             Microformats[name].attributeName,
  58.                                             Microformats[name].attributeValues);
  59.       
  60.     }
  61.     /* Create objects for the microformat nodes and put them into the microformats */
  62.     /* array */
  63.     for (let i = 0; i < microformatNodes.length; i++) {
  64.       targetArray.push(new Microformats[name].mfObject(microformatNodes[i]));
  65.     }
  66.     return targetArray;
  67.   },
  68.   /**
  69.    * Counts microformats objects of the given type from a document
  70.    * 
  71.    * @param  name          The name of the microformat (required)
  72.    * @param  rootElement   The DOM element at which to start searching (required)
  73.    * @param  recurseFrames Whether or not to search child frames for microformats (optional - defaults to true)
  74.    * @return The new count
  75.    */
  76.   count: function(name, rootElement, recurseFrames) {
  77.     if (!Microformats[name]) {
  78.       return;
  79.     }
  80.     var count = 0;
  81.  
  82.     rootElement = rootElement || content.document;
  83.  
  84.     /* If recurseFrames is undefined or true, look through all child frames for microformats */
  85.     if (recurseFrames || recurseFrames === undefined) {
  86.       if (rootElement.defaultView && rootElement.defaultView.frames.length > 0) {
  87.         for (let i=0; i < rootElement.defaultView.frames.length; i++) {
  88.           count += Microformats.count(name, rootElement.defaultView.frames[i].document, recurseFrames);
  89.         }
  90.       }
  91.     }
  92.  
  93.     /* Get the microformat nodes for the document */
  94.     var microformatNodes = [];
  95.     if (Microformats[name].className) {
  96.       microformatNodes = Microformats.getElementsByClassName(rootElement,
  97.                                         Microformats[name].className);
  98.       /* alternateClassName is for cases where a parent microformat is inferred by the children */
  99.       /* If we find alternateClassName, the entire document becomes the microformat */
  100.       if ((microformatNodes.length == 0) && Microformats[name].alternateClassName) {
  101.         var altClass = Microformats.getElementsByClassName(rootElement, Microformats[name].alternateClassName);
  102.         if (altClass.length > 0) {
  103.           microformatNodes.push(rootElement); 
  104.         }
  105.       }
  106.     } else if (Microformats[name].attributeValues) {
  107.       microformatNodes = 
  108.         Microformats.getElementsByAttribute(rootElement,
  109.                                             Microformats[name].attributeName,
  110.                                             Microformats[name].attributeValues);
  111.     }
  112.     count += microformatNodes.length;
  113.     return count;
  114.   },
  115.   /**
  116.    * Returns true if the passed in node is a microformat. Does NOT return true
  117.    * if the passed in node is a child of a microformat.
  118.    *
  119.    * @param  node          DOM node to check
  120.    * @return true if the node is a microformat, false if it is not
  121.    */
  122.   isMicroformat: function(node) {
  123.     for (let i in Microformats)
  124.     {
  125.       if (Microformats[i].className) {
  126.         if (Microformats.matchClass(node, Microformats[i].className)) {
  127.             return true;
  128.         }
  129.       } else {
  130.         var attribute;
  131.         if (attribute = node.getAttribute(Microformats[i].attributeName)) {
  132.           var attributeList = Microformats[i].attributeValues.split(" ");
  133.           for (let j=0; j < attributeList.length; j++) {
  134.             if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
  135.               return true;
  136.             }
  137.           }
  138.         }
  139.       }
  140.     }
  141.     return false;
  142.   },
  143.   /**
  144.    * This function searches a given nodes ancestors looking for a microformat
  145.    * and if it finds it, returns it. It does NOT include self, so if the passed
  146.    * in node is a microformat, it will still search ancestors for a microformat.
  147.    *
  148.    * @param  node          DOM node to check
  149.    * @return If the node is contained in a microformat, it returns the parent
  150.    *         DOM node, otherwise returns null
  151.    */
  152.   getParent: function(node) {
  153.     var xpathExpression;
  154.     var xpathResult;
  155.     var mfname;
  156.     for (let i in Microformats)
  157.     {
  158.       mfname = i;
  159.       if (Microformats[mfname]) {
  160.         if (Microformats[mfname].className) {
  161.           xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' " + Microformats[mfname].className + " ')]";
  162.         } else if (Microformats[mfname].attributeValues) {
  163.           xpathExpression = "ancestor::*[";
  164.           var attributeList = Microformats[i].attributeValues.split(" ");
  165.           for (let j=0; j < attributeList.length; j++) {
  166.             if (j != 0) {
  167.               xpathExpression += " or ";
  168.             }
  169.             xpathExpression += "contains(concat(' ', @" + Microformats[mfname].attributeName + ", ' '), ' " + attributeList[j] + " ')";
  170.           }
  171.           xpathExpression += "]"; 
  172.         } else {
  173.           continue;
  174.         }
  175.         xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
  176.         if (xpathResult.singleNodeValue) {
  177.           xpathResult.singleNodeValue.microformat = mfname;
  178.           return xpathResult.singleNodeValue;
  179.         }
  180.       }
  181.     }
  182.     return null;
  183.   },
  184.   /**
  185.    * If the passed in node is a microformat, this function returns a space 
  186.    * separated list of the microformat names that correspond to this node
  187.    *
  188.    * @param  node          DOM node to check
  189.    * @return If the node is a microformat, a space separated list of microformat
  190.    *         names, otherwise returns nothing
  191.    */
  192.   getNamesFromNode: function(node) {
  193.     var microformatNames = [];
  194.     var xpathExpression;
  195.     var xpathResult;
  196.     for (let i in Microformats)
  197.     {
  198.       if (Microformats[i]) {
  199.         if (Microformats[i].className) {
  200.           if (Microformats.matchClass(node, Microformats[i].className)) {
  201.             microformatNames.push(i);
  202.             continue;
  203.           }
  204.         } else if (Microformats[i].attributeValues) {
  205.           var attribute;
  206.           if (attribute = node.getAttribute(Microformats[i].attributeName)) {
  207.             var attributeList = Microformats[i].attributeValues.split(" ");
  208.             for (let j=0; j < attributeList.length; j++) {
  209.               /* If we match any attribute, we've got a microformat */
  210.               if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
  211.                 microformatNames.push(i);
  212.                 break;
  213.               }
  214.             }
  215.           }
  216.         }
  217.       }
  218.     }
  219.     return microformatNames.join(" ");
  220.   },
  221.   /**
  222.    * Outputs the contents of a microformat object for debug purposes.
  223.    *
  224.    * @param  microformatObject JavaScript object that represents a microformat
  225.    * @return string containing a visual representation of the contents of the microformat
  226.    */
  227.   debug: function debug(microformatObject) {
  228.     function dumpObject(item, indent)
  229.     {
  230.       if (!indent) {
  231.         indent = "";
  232.       }
  233.       var toreturn = "";
  234.       var testArray = [];
  235.       
  236.       for (let i in item)
  237.       {
  238.         if (testArray[i]) {
  239.           continue;
  240.         }
  241.         if (typeof item[i] == "object") {
  242.           if ((i != "node") && (i != "resolvedNode")) {
  243.             if (item[i] && item[i].semanticType) {
  244.               toreturn += indent + item[i].semanticType + " [" + i + "] { \n";
  245.             } else {
  246.               toreturn += indent + "object " + i + " { \n";
  247.             }
  248.             toreturn += dumpObject(item[i], indent + "\t");
  249.             toreturn += indent + "}\n";
  250.           }
  251.         } else if ((typeof item[i] != "function") && (i != "semanticType")) {
  252.           if (item[i]) {
  253.             toreturn += indent + i + "=" + item[i] + "\n";
  254.           }
  255.         }
  256.       }
  257.       if (!toreturn && item) {
  258.         toreturn = item.toString();
  259.       }
  260.       return toreturn;
  261.     }
  262.     return dumpObject(microformatObject);
  263.   },
  264.   add: function add(microformat, microformatDefinition) {
  265.     /* We always replace an existing definition with the new one */
  266.     if (microformatDefinition.mfVersion == Microformats.version) {
  267.       if (!Microformats[microformat]) {
  268.         Microformats.list.push(microformat);
  269.       }
  270.       Microformats[microformat] = microformatDefinition;
  271.       microformatDefinition.mfObject.prototype.debug =
  272.         function(microformatObject) {
  273.           return Microformats.debug(microformatObject)
  274.         };
  275.     }
  276.   },
  277.   /* All parser specific functions are contained in this object */
  278.   parser: {
  279.     /**
  280.      * Uses the microformat patterns to decide what the correct text for a
  281.      * given microformat property is. This includes looking at things like
  282.      * abbr, img/alt, area/alt and value excerpting.
  283.      *
  284.      * @param  propnode   The DOMNode to check
  285.      * @param  parentnode The parent node of the property. If it is a subproperty,
  286.      *                    this is the parent property node. If it is not, this is the
  287.      *                    microformat node.
  288.      & @param  datatype   HTML/text - whether to use innerHTML or innerText - defaults to text
  289.      * @return A string with the value of the property
  290.      */
  291.     defaultGetter: function(propnode, parentnode, datatype) {
  292.       if (((((propnode.localName.toLowerCase() == "abbr") || (propnode.localName.toLowerCase() == "html:abbr")) && !propnode.namespaceURI) || 
  293.          ((propnode.localName.toLowerCase() == "abbr") && (propnode.namespaceURI == "http://www.w3.org/1999/xhtml"))) && (propnode.getAttribute("title"))) {
  294.         return propnode.getAttribute("title");
  295.       } else if ((propnode.nodeName.toLowerCase() == "img") && (propnode.getAttribute("alt"))) {
  296.         return propnode.getAttribute("alt");
  297.       } else if ((propnode.nodeName.toLowerCase() == "area") && (propnode.getAttribute("alt"))) {
  298.         return propnode.getAttribute("alt");
  299.       } else if ((propnode.nodeName.toLowerCase() == "textarea") ||
  300.                  (propnode.nodeName.toLowerCase() == "select") ||
  301.                  (propnode.nodeName.toLowerCase() == "input")) {
  302.         return propnode.value;
  303.       } else {
  304.         var values = Microformats.getElementsByClassName(propnode, "value");
  305.         if (values.length > 0) {
  306.           var value = "";
  307.           for (let j=0;j<values.length;j++) {
  308.             value += Microformats.parser.defaultGetter(values[j], propnode, datatype);
  309.           }
  310.           return value;
  311.         } else {
  312.           var s;
  313.           if (datatype == "HTML") {
  314.             s = propnode.innerHTML;
  315.           } else {
  316.             if (propnode.innerText) {
  317.               s = propnode.innerText;
  318.             } else {
  319.               s = propnode.textContent;
  320.             }
  321.           }
  322.           /* If we are processing a value node, don't remove whitespace */
  323.           if (!Microformats.matchClass(propnode, "value")) {
  324.             /* Remove new lines, carriage returns and tabs */
  325.             s    = s.replace(/[\n\r\t]/gi, ' ');
  326.             /* Replace any double spaces with single spaces */
  327.             s    = s.replace(/\s{2,}/gi, ' ');
  328.             /* Remove any double spaces that are left */
  329.             s    = s.replace(/\s{2,}/gi, '');
  330.             /* Remove any spaces at the beginning */
  331.             s    = s.replace(/^\s+/, '');
  332.             /* Remove any spaces at the end */
  333.             s    = s.replace(/\s+$/, '');
  334.           }
  335.           if (s.length > 0) {
  336.             return s;
  337.           }
  338.         }
  339.       }
  340.     },
  341.     /**
  342.      * Used to specifically retrieve a date in a microformat node.
  343.      * After getting the default text, it normalizes it to an ISO8601 date.
  344.      *
  345.      * @param  propnode   The DOMNode to check
  346.      * @param  parentnode The parent node of the property. If it is a subproperty,
  347.      *                    this is the parent property node. If it is not, this is the
  348.      *                    microformat node.
  349.      * @return A string with the normalized date.
  350.      */
  351.     dateTimeGetter: function(propnode, parentnode) {
  352.       var date = Microformats.parser.textGetter(propnode, parentnode);
  353.       if (date) {
  354.         return Microformats.parser.normalizeISO8601(date);
  355.       }
  356.     },
  357.     /**
  358.      * Used to specifically retrieve a URI in a microformat node. This includes
  359.      * looking at an href/img/object/area to get the fully qualified URI.
  360.      *
  361.      * @param  propnode   The DOMNode to check
  362.      * @param  parentnode The parent node of the property. If it is a subproperty,
  363.      *                    this is the parent property node. If it is not, this is the
  364.      *                    microformat node.
  365.      * @return A string with the fully qualified URI.
  366.      */
  367.     uriGetter: function(propnode, parentnode) {
  368.       var pairs = {"a":"href", "img":"src", "object":"data", "area":"href"};
  369.       var name = propnode.nodeName.toLowerCase();
  370.       if (pairs.hasOwnProperty(name)) {
  371.         return propnode[pairs[name]];
  372.       }
  373.       return Microformats.parser.textGetter(propnode, parentnode);
  374.     },
  375.     /**
  376.      * Used to specifically retrieve a telephone number in a microformat node.
  377.      * Basically this is to handle the face that telephone numbers use value
  378.      * as the name as one of their subproperties, but value is also used for
  379.      * value excerpting (http://microformats.org/wiki/hcard#Value_excerpting)
  380.      
  381.      * @param  propnode   The DOMNode to check
  382.      * @param  parentnode The parent node of the property. If it is a subproperty,
  383.      *                    this is the parent property node. If it is not, this is the
  384.      *                    microformat node.
  385.      * @return A string with the telephone number
  386.      */
  387.     telGetter: function(propnode, parentnode) {
  388.       /* Special case - if this node is a value, use the parent node to get all the values */
  389.       if (Microformats.matchClass(propnode, "value")) {
  390.         return Microformats.parser.textGetter(parentnode, parentnode);
  391.       } else {
  392.         return Microformats.parser.textGetter(propnode, parentnode);
  393.       }
  394.     },
  395.     /**
  396.      * Used to specifically retrieve an email address in a microformat node.
  397.      * This includes at an href, as well as removing subject if specified and
  398.      * the mailto prefix.
  399.      *
  400.      * @param  propnode   The DOMNode to check
  401.      * @param  parentnode The parent node of the property. If it is a subproperty,
  402.      *                    this is the parent property node. If it is not, this is the
  403.      *                    microformat node.
  404.      * @return A string with the email address.
  405.      */
  406.     emailGetter: function(propnode, parentnode) {
  407.       if ((propnode.nodeName.toLowerCase() == "a") || (propnode.nodeName.toLowerCase() == "area")) {
  408.         var mailto = propnode.href;
  409.         /* IO Service won't fully parse mailto, so we do it manually */
  410.         if (mailto.indexOf('?') > 0) {
  411.           return unescape(mailto.substring("mailto:".length, mailto.indexOf('?')));
  412.         } else {
  413.           return unescape(mailto.substring("mailto:".length));
  414.         }
  415.       } else {
  416.         /* Special case - if this node is a value, use the parent node to get all the values */
  417.         /* If this case gets executed, per the value design pattern, the result */
  418.         /* will be the EXACT email address with no extra parsing required */
  419.         if (Microformats.matchClass(propnode, "value")) {
  420.           return Microformats.parser.textGetter(parentnode, parentnode);
  421.         } else {
  422.           return Microformats.parser.textGetter(propnode, parentnode);
  423.         }
  424.       }
  425.     },
  426.     /**
  427.      * Used when a caller needs the text inside a particular DOM node.
  428.      * It calls defaultGetter to handle all the subtleties of getting
  429.      * text from a microformat.
  430.      *
  431.      * @param  propnode   The DOMNode to check
  432.      * @param  parentnode The parent node of the property. If it is a subproperty,
  433.      *                    this is the parent property node. If it is not, this is the
  434.      *                    microformat node.
  435.      * @return A string with just the text including all tags.
  436.      */
  437.     textGetter: function(propnode, parentnode) {
  438.       return Microformats.parser.defaultGetter(propnode, parentnode, "text");
  439.     },
  440.     /**
  441.      * Used when a caller needs the HTML inside a particular DOM node.
  442.      *
  443.      * @param  propnode   The DOMNode to check
  444.      * @param  parentnode The parent node of the property. If it is a subproperty,
  445.      *                    this is the parent property node. If it is not, this is the
  446.      *                    microformat node.
  447.      * @return An object with function to access the string and the HTML
  448.      *         Note that because this is an object, you can't do string functions
  449.      *         so i faked a couple string functions that might be useful.
  450.      */
  451.     HTMLGetter: function(propnode, parentnode) {
  452.       return {
  453.         toString: function () {
  454.           return Microformats.parser.defaultGetter(propnode, parentnode, "text");
  455.         },
  456.         toHTML: function () {
  457.           return Microformats.parser.defaultGetter(propnode, parentnode, "HTML"); 
  458.         },
  459.         replace: function (a, b) {
  460.           return this.toString().replace(a,b);
  461.         },
  462.         match: function (a) {
  463.           return this.toString().match(a);
  464.         }
  465.       };
  466.     },
  467.     /**
  468.      * Internal parser API used to determine which getter to call based on the
  469.      * datatype specified in the microformat definition.
  470.      *
  471.      * @param  prop       The microformat property in the definition
  472.      * @param  propnode   The DOMNode to check
  473.      * @param  parentnode The parent node of the property. If it is a subproperty,
  474.      *                    this is the parent property node. If it is not, this is the
  475.      *                    microformat node.
  476.      * @return A string with the property value.
  477.      */
  478.     datatypeHelper: function(prop, node, parentnode) {
  479.       var result;
  480.       switch (prop.datatype) {
  481.         case "dateTime":
  482.           result = Microformats.parser.dateTimeGetter(node, parentnode);
  483.           break;
  484.         case "anyURI":
  485.           result = Microformats.parser.uriGetter(node, parentnode);
  486.           break;
  487.         case "email":
  488.           result = Microformats.parser.emailGetter(node, parentnode);
  489.           break;
  490.         case "tel":
  491.           result = Microformats.parser.telGetter(node, parentnode);
  492.           break;
  493.         case "HTML":
  494.           result = Microformats.parser.HTMLGetter(node, parentnode);
  495.           break;
  496.         case "float":
  497.           result = parseFloat(Microformats.parser.textGetter(node, parentnode));
  498.           break;
  499.         case "custom":
  500.           result = prop.customGetter(node, parentnode);
  501.           break;
  502.         case "microformat":
  503.           try {
  504.             result = new Microformats[prop.microformat].mfObject(node);
  505.           } catch (ex) {
  506.             /* We can swallow this exception. If the creation of the */
  507.             /* mf object fails, then the node isn't a microformat */
  508.           }
  509.           if (result) {
  510.             if (prop.microformat_property) {
  511.               result = result[prop.microformat_property];
  512.             }
  513.             break;
  514.           }
  515.         default:
  516.           result = Microformats.parser.textGetter(node, parentnode);
  517.           if ((prop.implied) && (result)) {
  518.             var temp = result;
  519.             result = {};
  520.             result[prop.implied] = temp;
  521.           }
  522.           break;
  523.       }
  524.       if (result && prop.types) {
  525.         var validType = false;
  526.         for (let type in prop.types) {
  527.           if (result.toLowerCase() == prop.types[type]) {
  528.             validType = true;
  529.             break;
  530.           }
  531.         }
  532.         if (!validType) {
  533.           return;
  534.         }
  535.       }
  536.       return result;
  537.     },
  538.     newMicroformat: function(object, in_node, microformat) {
  539.       /* check to see if we are even valid */
  540.       if (!Microformats[microformat]) {
  541.         throw("Invalid microformat - " + microformat);
  542.       }
  543.       if (in_node.ownerDocument) {
  544.         if (Microformats[microformat].attributeName) {
  545.           if (!(in_node.getAttribute(Microformats[microformat].attributeName))) {
  546.             throw("Node is not a microformat (" + microformat + ")");
  547.           }
  548.         } else {
  549.           if (!Microformats.matchClass(in_node, Microformats[microformat].className)) {
  550.             throw("Node is not a microformat (" + microformat + ")");
  551.           }
  552.         }
  553.       }
  554.       var node = in_node;
  555.       if ((Microformats[microformat].className) && in_node.ownerDocument) {
  556.         node = Microformats.parser.preProcessMicroformat(in_node);
  557.       }
  558.  
  559.       for (let i in Microformats[microformat].properties) {
  560.         object.__defineGetter__(i, Microformats.parser.getMicroformatPropertyGenerator(node, microformat, i, object));
  561.       }
  562.       
  563.       /* The node in the object should be the original node */
  564.       object.node = in_node;
  565.       /* we also store the node that has been "resolved" */
  566.       object.resolvedNode = node; 
  567.       object.semanticType = microformat;
  568.     },
  569.     getMicroformatPropertyGenerator: function getMicroformatPropertyGenerator(node, name, property, microformat)
  570.     {
  571.       return function() {
  572.         var result = Microformats.parser.getMicroformatProperty(node, name, property);
  573. //        delete microformat[property];
  574. //        microformat[property] = result; 
  575.         return result;
  576.       };
  577.     },
  578.     getPropertyInternal: function getPropertyInternal(propnode, parentnode, propobj, propname, mfnode) {
  579.       var result;
  580.       if (propobj.subproperties) {
  581.         for (let subpropname in propobj.subproperties) {
  582.           var subpropnodes;
  583.           var subpropobj = propobj.subproperties[subpropname];
  584.           if (subpropobj.rel == true) {
  585.             subpropnodes = Microformats.getElementsByAttribute(propnode, "rel", subpropname);
  586.           } else {
  587.             subpropnodes = Microformats.getElementsByClassName(propnode, subpropname);
  588.           }
  589.           var resultArray = [];
  590.           var subresult;
  591.           for (let i = 0; i < subpropnodes.length; i++) {
  592.             subresult = Microformats.parser.getPropertyInternal(subpropnodes[i], propnode,
  593.                                                                 subpropobj,
  594.                                                                 subpropname, mfnode);
  595.             if (subresult) {
  596.               resultArray.push(subresult);
  597.               /* If we're not a plural property, don't bother getting more */
  598.               if (!subpropobj.plural) {
  599.                 break;
  600.               }
  601.             }
  602.           }
  603.           if (resultArray.length == 0) {
  604.             subresult = Microformats.parser.getPropertyInternal(propnode, null,
  605.                                                                 subpropobj,
  606.                                                                 subpropname, mfnode);
  607.             if (subresult) {
  608.               resultArray.push(subresult);
  609.             }
  610.           }
  611.           if (resultArray.length > 0) {
  612.             result = result || {};
  613.             if (subpropobj.plural) {
  614.               result[subpropname] = resultArray;
  615.             } else {
  616.               result[subpropname] = resultArray[0];
  617.             }
  618.           }
  619.         }
  620.       }
  621.       if (!parentnode || (!result && propobj.subproperties)) {
  622.         if (propobj.virtual) {
  623.           if (propobj.virtualGetter) {
  624.             result = propobj.virtualGetter(mfnode || propnode);
  625.           } else {
  626.             result = Microformats.parser.datatypeHelper(propobj, propnode);
  627.           }
  628.         } else if (propobj.implied) {
  629.           result = Microformats.parser.datatypeHelper(propobj, propnode);
  630.         }
  631.       } else if (!result) {
  632.         result = Microformats.parser.datatypeHelper(propobj, propnode, parentnode);
  633.       }
  634.       return result;
  635.     },
  636.     getMicroformatProperty: function getMicroformatProperty(in_mfnode, mfname, propname) {
  637.       var mfnode = in_mfnode;
  638.       /* If the node has not been preprocessed, the requested microformat */
  639.       /* is a class based microformat and the passed in node is not the */
  640.       /* entire document, preprocess it. Preprocessing the node involves */
  641.       /* creating a duplicate of the node and taking care of things like */
  642.       /* the include and header design patterns */
  643.       if (!in_mfnode.origNode && Microformats[mfname].className && in_mfnode.ownerDocument) {
  644.         mfnode = Microformats.parser.preProcessMicroformat(in_mfnode);
  645.       }
  646.       /* propobj is the corresponding property object in the microformat */
  647.       var propobj;
  648.       /* If there is a corresponding property in the microformat, use it */
  649.       if (Microformats[mfname].properties[propname]) {
  650.         propobj = Microformats[mfname].properties[propname];
  651.       } else {
  652.         /* If we didn't get a property, bail */
  653.         return;
  654.       }
  655.       /* Query the correct set of nodes (rel or class) based on the setting */
  656.       /* in the property */
  657.       var propnodes;
  658.       if (propobj.rel == true) {
  659.         propnodes = Microformats.getElementsByAttribute(mfnode, "rel", propname);
  660.       } else {
  661.         propnodes = Microformats.getElementsByClassName(mfnode, propname);
  662.       }
  663.       if (propnodes.length > 0) {
  664.         var resultArray = [];
  665.         for (let i = 0; i < propnodes.length; i++) {
  666.           var subresult = Microformats.parser.getPropertyInternal(propnodes[i],
  667.                                                                   mfnode,
  668.                                                                   propobj,
  669.                                                                   propname);
  670.           if (subresult) {
  671.             resultArray.push(subresult);
  672.             /* If we're not a plural property, don't bother getting more */
  673.             if (!propobj.plural) {
  674.               return resultArray[0];
  675.             }
  676.           }
  677.         }
  678.         if (resultArray.length > 0) {
  679.           return resultArray;
  680.         }
  681.       } else {
  682.         /* If we didn't find any class nodes, check to see if this property */
  683.         /* is virtual and if so, call getPropertyInternal again */
  684.         if (propobj.virtual) {
  685.           return Microformats.parser.getPropertyInternal(mfnode, null,
  686.                                                          propobj, propname);
  687.         }
  688.       }
  689.       return;
  690.     },
  691.     /**
  692.      * Internal parser API used to resolve includes and headers. Includes are
  693.      * resolved by simply cloning the node and replacing it in a clone of the
  694.      * original DOM node. Headers are resolved by creating a span and then copying
  695.      * the innerHTML and the class name.
  696.      *
  697.      * @param  in_mfnode The node to preProcess.
  698.      * @return If the node had includes or headers, a cloned node otherwise
  699.      *         the original node. You can check to see if the node was cloned
  700.      *         by looking for .origNode in the new node.
  701.      */
  702.     preProcessMicroformat: function preProcessMicroformat(in_mfnode) {
  703.       var mfnode;
  704.       var includes = Microformats.getElementsByClassName(in_mfnode, "include");
  705.       if ((includes.length > 0) || ((in_mfnode.nodeName.toLowerCase() == "td") && (in_mfnode.getAttribute("headers")))) {
  706.         mfnode = in_mfnode.cloneNode(true);
  707.         mfnode.origNode = in_mfnode;
  708.         if (includes.length > 0) {
  709.           includes = Microformats.getElementsByClassName(mfnode, "include");
  710.           var includeId;
  711.           var include_length = includes.length;
  712.           for (let i = include_length -1; i >= 0; i--) {
  713.             if (includes[i].nodeName.toLowerCase() == "a") {
  714.               includeId = includes[i].getAttribute("href").substr(1);
  715.             }
  716.             if (includes[i].nodeName.toLowerCase() == "object") {
  717.               includeId = includes[i].getAttribute("data").substr(1);
  718.             }
  719.             includes[i].parentNode.replaceChild(in_mfnode.ownerDocument.getElementById(includeId).cloneNode(true), includes[i]);
  720.           }
  721.         } else {
  722.           var headers = in_mfnode.getAttribute("headers").split(" ");
  723.           for (let i = 0; i < headers.length; i++) {
  724.             var tempNode = in_mfnode.ownerDocument.createElement("span");
  725.             var headerNode = in_mfnode.ownerDocument.getElementById(headers[i]);
  726.             tempNode.innerHTML = headerNode.innerHTML;
  727.             tempNode.className = headerNode.className;
  728.             mfnode.appendChild(tempNode);
  729.           }
  730.         }
  731.       } else {
  732.         mfnode = in_mfnode;
  733.       }
  734.       return mfnode;
  735.     },
  736.     validate: function validate(mfnode, mfname, error) {
  737.       if (Microformats[mfname].validate) {
  738.         return Microformats[mfname].validate(mfnode, error);
  739.       } else {
  740.         var mfobject = new Microformats[mfname].mfObject(mfnode);
  741.         if (Microformats[mfname].required) {
  742.           error.message = "";
  743.           for (let i=0;i<Microformats[mfname].required.length;i++) {
  744.             if (!mfobject[Microformats[mfname].required[i]]) {
  745.               error.message += "Required property " + Microformats[mfname].required[i] + " not specified\n";
  746.             }
  747.           }
  748.           if (error.message.length > 0) {
  749.             return false;
  750.           }
  751.         } else {
  752.           if (!mfobject.toString()) {
  753.             error.message = "Unable to create microformat";
  754.             return false;
  755.           }
  756.         }
  757.         return true;
  758.       }
  759.     },
  760.     /* This function normalizes an ISO8601 date by adding punctuation and */
  761.     /* ensuring that hours and seconds have values */
  762.     normalizeISO8601: function normalizeISO8601(string)
  763.     {
  764.       var dateArray = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(?:([-+Z])(?:(\d\d)(?::?(\d\d))?)?)?)?)?)?/);
  765.   
  766.       var dateString;
  767.       var tzOffset = 0;
  768.       if (!dateArray) {
  769.         return;
  770.       }
  771.       if (dateArray[1]) {
  772.         dateString = dateArray[1];
  773.         if (dateArray[2]) {
  774.           dateString += "-" + dateArray[2];
  775.           if (dateArray[3]) {
  776.             dateString += "-" + dateArray[3];
  777.             if (dateArray[4]) {
  778.               dateString += "T" + dateArray[4];
  779.               if (dateArray[5]) {
  780.                 dateString += ":" + dateArray[5];
  781.               } else {
  782.                 dateString += ":" + "00";
  783.               }
  784.               if (dateArray[6]) {
  785.                 dateString += ":" + dateArray[6];
  786.               } else {
  787.                 dateString += ":" + "00";
  788.               }
  789.               if (dateArray[7]) {
  790.                 dateString += "." + dateArray[7];
  791.               }
  792.               if (dateArray[8]) {
  793.                 dateString += dateArray[8];
  794.                 if ((dateArray[8] == "+") || (dateArray[8] == "-")) {
  795.                   if (dateArray[9]) {
  796.                     dateString += dateArray[9];
  797.                     if (dateArray[10]) {
  798.                       dateString += dateArray[10];
  799.                     }
  800.                   }
  801.                 }
  802.               }
  803.             }
  804.           }
  805.         }
  806.       }
  807.       return dateString;
  808.     }
  809.   },
  810.   /**
  811.    * Converts an ISO8601 date into a JavaScript date object, honoring the TZ
  812.    * offset and Z if present to convert the date to local time
  813.    * NOTE: I'm using an extra parameter on the date object for this function.
  814.    * I set date.time to true if there is a date, otherwise date.time is false.
  815.    * 
  816.    * @param  string ISO8601 formatted date
  817.    * @return JavaScript date object that represents the ISO date. 
  818.    */
  819.   dateFromISO8601: function dateFromISO8601(string) {
  820.     var dateArray = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(?:([-+Z])(?:(\d\d)(?::?(\d\d))?)?)?)?)?)?/);
  821.   
  822.     var date = new Date(dateArray[1], 0, 1);
  823.     date.time = false;
  824.  
  825.     if (dateArray[2]) {
  826.       date.setMonth(dateArray[2] - 1);
  827.     }
  828.     if (dateArray[3]) {
  829.       date.setDate(dateArray[3]);
  830.     }
  831.     if (dateArray[4]) {
  832.       date.setHours(dateArray[4]);
  833.       date.time = true;
  834.       if (dateArray[5]) {
  835.         date.setMinutes(dateArray[5]);
  836.         if (dateArray[6]) {
  837.           date.setSeconds(dateArray[6]);
  838.           if (dateArray[7]) {
  839.             date.setMilliseconds(Number("0." + dateArray[7]) * 1000);
  840.           }
  841.         }
  842.       }
  843.     }
  844.     if (dateArray[8]) {
  845.       if (dateArray[8] == "-") {
  846.         if (dateArray[9] && dateArray[10]) {
  847.           date.setHours(date.getHours() + parseInt(dateArray[9], 10));
  848.           date.setMinutes(date.getMinutes() + parseInt(dateArray[10], 10));
  849.         }
  850.       } else if (dateArray[8] == "+") {
  851.         if (dateArray[9] && dateArray[10]) {
  852.           date.setHours(date.getHours() - parseInt(dateArray[9], 10));
  853.           date.setMinutes(date.getMinutes() - parseInt(dateArray[10], 10));
  854.         }
  855.       }
  856.       /* at this point we have the time in gmt */
  857.       /* convert to local if we had a Z - or + */
  858.       if (dateArray[8]) {
  859.         var tzOffset = date.getTimezoneOffset();
  860.         if (tzOffset < 0) {
  861.           date.setMinutes(date.getMinutes() + tzOffset); 
  862.         } else if (tzOffset > 0) {
  863.           date.setMinutes(date.getMinutes() - tzOffset); 
  864.         }
  865.       }
  866.     }
  867.     return date;
  868.   },
  869.   /**
  870.    * Converts a Javascript date object into an ISO 8601 formatted date
  871.    * NOTE: I'm using an extra parameter on the date object for this function.
  872.    * If date.time is NOT true, this function only outputs the date.
  873.    * 
  874.    * @param  date        Javascript Date object
  875.    * @param  punctuation true if the date should have -/:
  876.    * @return string with the ISO date. 
  877.    */
  878.   iso8601FromDate: function iso8601FromDate(date, punctuation) {
  879.     var string = date.getFullYear().toString();
  880.     if (punctuation) {
  881.       string += "-";
  882.     }
  883.     string += (date.getMonth() + 1).toString().replace(/\b(\d)\b/g, '0$1');
  884.     if (punctuation) {
  885.       string += "-";
  886.     }
  887.     string += date.getDate().toString().replace(/\b(\d)\b/g, '0$1');
  888.     if (date.time) {
  889.       string += "T";
  890.       string += date.getHours().toString().replace(/\b(\d)\b/g, '0$1');
  891.       if (punctuation) {
  892.         string += ":";
  893.       }
  894.       string += date.getMinutes().toString().replace(/\b(\d)\b/g, '0$1');
  895.       if (punctuation) {
  896.         string += ":";
  897.       }
  898.       string += date.getSeconds().toString().replace(/\b(\d)\b/g, '0$1');
  899.       if (date.getMilliseconds() > 0) {
  900.         if (punctuation) {
  901.           string += ".";
  902.         }
  903.         string += date.getMilliseconds().toString();
  904.       }
  905.     }
  906.     return string;
  907.   },
  908.   simpleEscape: function simpleEscape(s)
  909.   {
  910.     s = s.replace(/\&/g, '%26');
  911.     s = s.replace(/\#/g, '%23');
  912.     s = s.replace(/\+/g, '%2B');
  913.     s = s.replace(/\-/g, '%2D');
  914.     s = s.replace(/\=/g, '%3D');
  915.     s = s.replace(/\'/g, '%27');
  916.     s = s.replace(/\,/g, '%2C');
  917. //    s = s.replace(/\r/g, '%0D');
  918. //    s = s.replace(/\n/g, '%0A');
  919.     s = s.replace(/ /g, '+');
  920.     return s;
  921.   },
  922.   /**
  923.    * Not intended for external consumption. Microformat implementations might use it.
  924.    *
  925.    * Retrieve elements matching all classes listed in a space-separated string.
  926.    * I had to implement my own because I need an Array, not an nsIDomNodeList
  927.    * 
  928.    * @param  rootElement      The DOM element at which to start searching (optional)
  929.    * @param  className        A space separated list of classenames
  930.    * @return microformatNodes An array of DOM Nodes, each representing a
  931.                               microformat in the document.
  932.    */
  933.   getElementsByClassName: function getElementsByClassName(rootNode, className)
  934.   {
  935.     var returnElements = [];
  936.  
  937.     if ((rootNode.ownerDocument || rootNode).getElementsByClassName) {
  938.     /* Firefox 3 - native getElementsByClassName */
  939.       var col = rootNode.getElementsByClassName(className);
  940.       for (let i = 0; i < col.length; i++) {
  941.         returnElements[i] = col[i];
  942.       }
  943.     } else if ((rootNode.ownerDocument || rootNode).evaluate) {
  944.     /* Firefox 2 and below - XPath */
  945.       var xpathExpression;
  946.       xpathExpression = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
  947.       var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  948.  
  949.       var node;
  950.       while (node = xpathResult.iterateNext()) {
  951.         returnElements.push(node);
  952.       }
  953.     } else {
  954.     /* Slow fallback for testing */
  955.       className = className.replace(/\-/g, "\\-");
  956.       var elements = rootNode.getElementsByTagName("*");
  957.       for (let i=0;i<elements.length;i++) {
  958.         if (elements[i].className.match("(^|\\s)" + className + "(\\s|$)")) {
  959.           returnElements.push(elements[i]);
  960.         }
  961.       }
  962.     }
  963.     return returnElements;
  964.   },
  965.   /**
  966.    * Not intended for external consumption. Microformat implementations might use it.
  967.    *
  968.    * Retrieve elements matching an attribute and an attribute list in a space-separated string.
  969.    * 
  970.    * @param  rootElement      The DOM element at which to start searching (optional)
  971.    * @param  atributeName     The attribute name to match against
  972.    * @param  attributeValues  A space separated list of attribute values
  973.    * @return microformatNodes An array of DOM Nodes, each representing a
  974.                               microformat in the document.
  975.    */
  976.   getElementsByAttribute: function getElementsByAttribute(rootNode, attributeName, attributeValues)
  977.   {
  978.     var attributeList = attributeValues.split(" ");
  979.  
  980.     var returnElements = [];
  981.  
  982.     if ((rootNode.ownerDocument || rootNode).evaluate) {
  983.     /* Firefox 3 and below - XPath */
  984.       /* Create an XPath expression based on the attribute list */
  985.       var xpathExpression = ".//*[";
  986.       for (let i = 0; i < attributeList.length; i++) {
  987.         if (i != 0) {
  988.           xpathExpression += " or ";
  989.         }
  990.         xpathExpression += "contains(concat(' ', @" + attributeName + ", ' '), ' " + attributeList[i] + " ')";
  991.       }
  992.       xpathExpression += "]"; 
  993.  
  994.       var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  995.  
  996.       var node;
  997.       while (node = xpathResult.iterateNext()) {
  998.         returnElements.push(node);
  999.       }
  1000.     } else {
  1001.     /* Need Slow fallback for testing */
  1002.     }
  1003.     return returnElements;
  1004.   },
  1005.   matchClass: function matchClass(node, className) {
  1006.     var classValue = node.getAttribute("class");
  1007.     return (classValue && classValue.match("(^|\\s)" + className + "(\\s|$)"));
  1008.   }
  1009. };
  1010.  
  1011. /* MICROFORMAT DEFINITIONS BEGIN HERE */
  1012.  
  1013. function adr(node) {
  1014.   if (node) {
  1015.     Microformats.parser.newMicroformat(this, node, "adr");
  1016.   }
  1017. }
  1018.  
  1019. adr.prototype.toString = function() {
  1020.   var address_text = "";
  1021.   var start_parens = false;
  1022.   if (this["street-address"]) {
  1023.     address_text += this["street-address"][0];
  1024.     address_text += " ";
  1025.   }
  1026.   if (this["locality"]) {
  1027.     if (this["street-address"]) {
  1028.       address_text += "(";
  1029.       start_parens = true;
  1030.     }
  1031.     address_text += this["locality"];
  1032.   }
  1033.   if (this["region"]) {
  1034.     if ((this["street-address"]) && (!start_parens)) {
  1035.       address_text += "(";
  1036.       start_parens = true;
  1037.     } else if (this["locality"]) {
  1038.       address_text += ", ";
  1039.     }
  1040.     address_text += this["region"];
  1041.   }
  1042.   if (this["country-name"]) {
  1043.     if ((this["street-address"]) && (!start_parens)) {
  1044.       address_text += "(";
  1045.       start_parens = true;
  1046.       address_text += this["country-name"];
  1047.     } else if ((!this["locality"]) && (!this["region"])) {
  1048.       address_text += this["country-name"];
  1049.     } else if (((!this["locality"]) && (this["region"])) || ((this["locality"]) && (!this["region"]))) {
  1050.       address_text += ", ";
  1051.       address_text += this["country-name"];
  1052.     }
  1053.   }
  1054.   if (start_parens) {
  1055.     address_text += ")";
  1056.   }
  1057.   return address_text;
  1058. }
  1059.  
  1060. var adr_definition = {
  1061.   mfVersion: 0.8,
  1062.   mfObject: adr,
  1063.   className: "adr",
  1064.   properties: {
  1065.     "type" : {
  1066.       plural: true,
  1067.       types: ["work", "home", "pref", "postal", "dom", "intl", "parcel"]
  1068.     },
  1069.     "post-office-box" : {
  1070.     },
  1071.     "street-address" : {
  1072.       plural: true
  1073.     },
  1074.     "extended-address" : {
  1075.     },
  1076.     "locality" : {
  1077.     },
  1078.     "region" : {
  1079.     },
  1080.     "postal-code" : {
  1081.     },
  1082.     "country-name" : {
  1083.     }
  1084.   }
  1085. };
  1086.  
  1087. Microformats.add("adr", adr_definition);
  1088.  
  1089. function hCard(node) {
  1090.   if (node) {
  1091.     Microformats.parser.newMicroformat(this, node, "hCard");
  1092.   }
  1093. }
  1094. hCard.prototype.toString = function() {
  1095.   if (this.resolvedNode) {
  1096.     /* If this microformat has an include pattern, put the */
  1097.     /* organization-name in parenthesis after the fn to differentiate */
  1098.     /* them. */
  1099.     var fns = Microformats.getElementsByClassName(this.node, "fn");
  1100.     if (fns.length === 0) {
  1101.       if (this.fn) {
  1102.         if (this.org && this.org[0]["organization-name"] && (this.fn != this.org[0]["organization-name"])) {
  1103.           return this.fn + " (" + this.org[0]["organization-name"] + ")";
  1104.         }
  1105.       }
  1106.     }
  1107.   }
  1108.   return this.fn;
  1109. }
  1110.  
  1111. var hCard_definition = {
  1112.   mfVersion: 0.8,
  1113.   mfObject: hCard,
  1114.   className: "vcard",
  1115.   required: ["fn"],
  1116.   properties: {
  1117.     "adr" : {
  1118.       plural: true,
  1119.       datatype: "microformat",
  1120.       microformat: "adr"
  1121.     },
  1122.     "agent" : {
  1123.       plural: true
  1124.     },
  1125.     "bday" : {
  1126.       datatype: "dateTime"
  1127.     },
  1128.     "class" : {
  1129.     },
  1130.     "category" : {
  1131.       plural: true,
  1132.       datatype: "microformat",
  1133.       microformat: "tag",
  1134.       microformat_property: "tag"
  1135.     },
  1136.     "email" : {
  1137.       subproperties: {
  1138.         "type" : {
  1139.           plural: true,
  1140.           types: ["internet", "x400", "pref"]
  1141.         },
  1142.         "value" : {
  1143.           datatype: "email",
  1144.           virtual: true
  1145.         }
  1146.       },
  1147.       plural: true   
  1148.     },
  1149.     "fn" : {
  1150.       required: true
  1151.     },
  1152.     "geo" : {
  1153.       value: "geo",
  1154.       datatype: "microformat",
  1155.       microformat: "geo"
  1156.     },
  1157.     "key" : {
  1158.       plural: true
  1159.     },
  1160.     "label" : {
  1161.       plural: true
  1162.     },
  1163.     "logo" : {
  1164.       plural: true,
  1165.       datatype: "anyURI"
  1166.     },
  1167.     "mailer" : {
  1168.       plural: true
  1169.     },
  1170.     "n" : {
  1171.       subproperties: {
  1172.         "honorific-prefix" : {
  1173.           plural: true
  1174.         },
  1175.         "given-name" : {
  1176.         },
  1177.         "additional-name" : {
  1178.           plural: true
  1179.         },
  1180.         "family-name" : {
  1181.         },
  1182.         "honorific-suffix" : {
  1183.           plural: true
  1184.         }
  1185.       },
  1186.       virtual: true,
  1187.       /*  Implied "n" Optimization */
  1188.       /* http://microformats.org/wiki/hcard#Implied_.22n.22_Optimization */
  1189.       virtualGetter: function(mfnode) {
  1190.         var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1191.         var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1192.         var given_name;
  1193.         var family_name;
  1194.         if (fn && (!orgs || (orgs.length > 1) || (fn != orgs[0]["organization-name"]))) {
  1195.           var fns = fn.split(" ");
  1196.           if (fns.length === 2) {
  1197.             if (fns[0].charAt(fns[0].length-1) == ',') {
  1198.               given_name = fns[1];
  1199.               family_name = fns[0].substr(0, fns[0].length-1);
  1200.             } else if (fns[1].length == 1) {
  1201.               given_name = fns[1];
  1202.               family_name = fns[0];
  1203.             } else if ((fns[1].length == 2) && (fns[1].charAt(fns[1].length-1) == '.')) {
  1204.               given_name = fns[1];
  1205.               family_name = fns[0];
  1206.             } else {
  1207.               given_name = fns[0];
  1208.               family_name = fns[1];
  1209.             }
  1210.             return {"given-name" : given_name, "family-name" : family_name};
  1211.           }
  1212.         }
  1213.       }
  1214.     },
  1215.     "nickname" : {
  1216.       plural: true,
  1217.       virtual: true,
  1218.       /* Implied "nickname" Optimization */
  1219.       /* http://microformats.org/wiki/hcard#Implied_.22nickname.22_Optimization */
  1220.       virtualGetter: function(mfnode) {
  1221.         var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1222.         var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1223.         var given_name;
  1224.         var family_name;
  1225.         if (fn && (!orgs || (orgs.length) > 1 || (fn != orgs[0]["organization-name"]))) {
  1226.           var fns = fn.split(" ");
  1227.           if (fns.length === 1) {
  1228.             return [fns[0]];
  1229.           }
  1230.         }
  1231.         return;
  1232.       }
  1233.     },
  1234.     "note" : {
  1235.       plural: true,
  1236.       datatype: "HTML"
  1237.     },
  1238.     "org" : {
  1239.       subproperties: {
  1240.         "organization-name" : {
  1241.         },
  1242.         "organization-unit" : {
  1243.           plural: true
  1244.         }
  1245.       },
  1246.       plural: true,
  1247.       implied: "organization-name"
  1248.     },
  1249.     "photo" : {
  1250.       plural: true,
  1251.       datatype: "anyURI"
  1252.     },
  1253.     "rev" : {
  1254.       datatype: "dateTime"
  1255.     },
  1256.     "role" : {
  1257.       plural: true
  1258.     },
  1259.     "sequence" : {
  1260.     },
  1261.     "sort-string" : {
  1262.     },
  1263.     "sound" : {
  1264.       plural: true
  1265.     },
  1266.     "title" : {
  1267.       plural: true
  1268.     },
  1269.     "tel" : {
  1270.       subproperties: {
  1271.         "type" : {
  1272.           plural: true,
  1273.           types: ["msg", "home", "work", "pref", "voice", "fax", "cell", "video", "pager", "bbs", "car", "isdn", "pcs"]
  1274.         },
  1275.         "value" : {
  1276.           datatype: "tel"
  1277.         }
  1278.       },
  1279.       plural: true,
  1280.       implied: "value"
  1281.     },
  1282.     "tz" : {
  1283.     },
  1284.     "uid" : {
  1285.       datatype: "anyURI"
  1286.     },
  1287.     "url" : {
  1288.       plural: true,
  1289.       datatype: "anyURI"
  1290.     }
  1291.   }
  1292. };
  1293.  
  1294. Microformats.add("hCard", hCard_definition);
  1295.  
  1296. function hCalendar(node) {
  1297.   if (node) {
  1298.     Microformats.parser.newMicroformat(this, node, "hCalendar");
  1299.   }
  1300. }
  1301. hCalendar.prototype.toString = function() {
  1302.   if (this.resolvedNode) {
  1303.     /* If this microformat has an include pattern, put the */
  1304.     /* dtstart in parenthesis after the summary to differentiate */
  1305.     /* them. */
  1306.     var summaries = Microformats.getElementsByClassName(this.node, "summary");
  1307.     if (summaries.length === 0) {
  1308.       if (this.summary) {
  1309.         if (this.dtstart) {
  1310.           return this.summary + " (" + Microformats.dateFromISO8601(this.dtstart).toLocaleString() + ")";
  1311.         }
  1312.       }
  1313.     }
  1314.   }
  1315.   if (this.dtstart) {
  1316.     return this.summary;
  1317.   }
  1318.   return;
  1319. }
  1320.  
  1321. var hCalendar_definition = {
  1322.   mfVersion: 0.8,
  1323.   mfObject: hCalendar,
  1324.   className: "vevent",
  1325.   required: ["summary", "dtstart"],
  1326.   properties: {
  1327.     "category" : {
  1328.       plural: true,
  1329.       datatype: "microformat",
  1330.       microformat: "tag",
  1331.       microformat_property: "tag"
  1332.     },
  1333.     "class" : {
  1334.       types: ["public", "private", "confidential"]
  1335.     },
  1336.     "description" : {
  1337.       datatype: "HTML"
  1338.     },
  1339.     "dtstart" : {
  1340.       datatype: "dateTime"
  1341.     },
  1342.     "dtend" : {
  1343.       datatype: "dateTime"
  1344.     },
  1345.     "dtstamp" : {
  1346.       datatype: "dateTime"
  1347.     },
  1348.     "duration" : {
  1349.     },
  1350.     "geo" : {
  1351.       value: "geo",
  1352.       datatype: "microformat",
  1353.       microformat: "geo"
  1354.     },
  1355.     "location" : {
  1356.       datatype: "microformat",
  1357.       microformat: "hCard"
  1358.     },
  1359.     "status" : {
  1360.       types: ["tentative", "confirmed", "cancelled"]
  1361.     },
  1362.     "summary" : {},
  1363.     "transp" : {
  1364.       types: ["opaque", "transparent"]
  1365.     },
  1366.     "uid" : {
  1367.       datatype: "anyURI"
  1368.     },
  1369.     "url" : {
  1370.       datatype: "anyURI"
  1371.     },
  1372.     "last-modified" : {
  1373.       datatype: "dateTime"
  1374.     },
  1375.     "rrule" : {
  1376.       subproperties: {
  1377.         "interval" : {
  1378.           virtual: true,
  1379.           /* This will only be called in the virtual case */
  1380.           virtualGetter: function(mfnode) {
  1381.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "interval");
  1382.           }
  1383.         },
  1384.         "freq" : {
  1385.           virtual: true,
  1386.           /* This will only be called in the virtual case */
  1387.           virtualGetter: function(mfnode) {
  1388.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "freq");
  1389.           }
  1390.         },
  1391.         "bysecond" : {
  1392.           virtual: true,
  1393.           /* This will only be called in the virtual case */
  1394.           virtualGetter: function(mfnode) {
  1395.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bysecond");
  1396.           }
  1397.         },
  1398.         "byminute" : {
  1399.           virtual: true,
  1400.           /* This will only be called in the virtual case */
  1401.           virtualGetter: function(mfnode) {
  1402.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byminute");
  1403.           }
  1404.         },
  1405.         "byhour" : {
  1406.           virtual: true,
  1407.           /* This will only be called in the virtual case */
  1408.           virtualGetter: function(mfnode) {
  1409.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byhour");
  1410.           }
  1411.         },
  1412.         "bymonthday" : {
  1413.           virtual: true,
  1414.           /* This will only be called in the virtual case */
  1415.           virtualGetter: function(mfnode) {
  1416.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonthday");
  1417.           }
  1418.         },
  1419.         "byyearday" : {
  1420.           virtual: true,
  1421.           /* This will only be called in the virtual case */
  1422.           virtualGetter: function(mfnode) {
  1423.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byyearday");
  1424.           }
  1425.         },
  1426.         "byweekno" : {
  1427.           virtual: true,
  1428.           /* This will only be called in the virtual case */
  1429.           virtualGetter: function(mfnode) {
  1430.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byweekno");
  1431.           }
  1432.         },
  1433.         "bymonth" : {
  1434.           virtual: true,
  1435.           /* This will only be called in the virtual case */
  1436.           virtualGetter: function(mfnode) {
  1437.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonth");
  1438.           }
  1439.         },
  1440.         "byday" : {
  1441.           virtual: true,
  1442.           /* This will only be called in the virtual case */
  1443.           virtualGetter: function(mfnode) {
  1444.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byday");
  1445.           }
  1446.         },
  1447.         "until" : {
  1448.           virtual: true,
  1449.           /* This will only be called in the virtual case */
  1450.           virtualGetter: function(mfnode) {
  1451.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "until");
  1452.           }
  1453.         },
  1454.         "count" : {
  1455.           virtual: true,
  1456.           /* This will only be called in the virtual case */
  1457.           virtualGetter: function(mfnode) {
  1458.             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "count");
  1459.           }
  1460.         }
  1461.       },
  1462.       retrieve: function(mfnode, property) {
  1463.         var value = Microformats.parser.textGetter(mfnode);
  1464.         var rrule;
  1465.         rrule = value.split(';');
  1466.         for (let i=0; i < rrule.length; i++) {
  1467.           if (rrule[i].match(property)) {
  1468.             return rrule[i].split('=')[1];
  1469.           }
  1470.         }
  1471.       }
  1472.     }
  1473.   }
  1474. };
  1475.  
  1476. Microformats.add("hCalendar", hCalendar_definition);
  1477.  
  1478. function geo(node) {
  1479.   if (node) {
  1480.     Microformats.parser.newMicroformat(this, node, "geo");
  1481.   }
  1482. }
  1483. geo.prototype.toString = function() {
  1484.   if (this.latitude && this.longitude) {
  1485.     var s;
  1486.     if ((this.node.localName.toLowerCase() != "abbr") && (this.node.localName.toLowerCase() == "html:abbr")) {
  1487.       s = Microformats.parser.textGetter(this.node);
  1488.     } else {
  1489.       s = this.node.textContent;
  1490.     }
  1491.  
  1492.     /* FIXME - THIS IS FIREFOX SPECIFIC */
  1493.     /* check if geo is contained in a vcard */
  1494.     var xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vcard ')]";
  1495.     var xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
  1496.     if (xpathResult.singleNodeValue) {
  1497.       var hcard = new hCard(xpathResult.singleNodeValue);
  1498.       if (hcard.fn) {
  1499.         return hcard.fn;
  1500.       }
  1501.     }
  1502.     /* check if geo is contained in a vevent */
  1503.     xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vevent ')]";
  1504.     xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, xpathResult);
  1505.     if (xpathResult.singleNodeValue) {
  1506.       var hcal = new hCalendar(xpathResult.singleNodeValue);
  1507.       if (hcal.summary) {
  1508.         return hcal.summary;
  1509.       }
  1510.     }
  1511.     if (s) {
  1512.       return s;
  1513.     } else {
  1514.       return this.latitude + ", " + this.longitude;
  1515.     }
  1516.   }
  1517. }
  1518.  
  1519. var geo_definition = {
  1520.   mfVersion: 0.8,
  1521.   mfObject: geo,
  1522.   className: "geo",
  1523.   required: ["latitude","longitude"],
  1524.   properties: {
  1525.     "latitude" : {
  1526.       datatype: "float",
  1527.       virtual: true,
  1528.       /* This will only be called in the virtual case */
  1529.       virtualGetter: function(mfnode) {
  1530.         var value = Microformats.parser.textGetter(mfnode);
  1531.         var latlong;
  1532.         if (value.match(';')) {
  1533.           latlong = value.split(';');
  1534.           if (latlong[0]) {
  1535.             return parseFloat(latlong[0]);
  1536.           }
  1537.         }
  1538.       }
  1539.     },
  1540.     "longitude" : {
  1541.       datatype: "float",
  1542.       virtual: true,
  1543.       /* This will only be called in the virtual case */
  1544.       virtualGetter: function(mfnode) {
  1545.         var value = Microformats.parser.textGetter(mfnode);
  1546.         var latlong;
  1547.         if (value.match(';')) {
  1548.           latlong = value.split(';');
  1549.           if (latlong[1]) {
  1550.             return parseFloat(latlong[1]);
  1551.           }
  1552.         }
  1553.       }
  1554.     }
  1555.   }
  1556. };
  1557.  
  1558. Microformats.add("geo", geo_definition);
  1559.  
  1560. function tag(node) {
  1561.   if (node) {
  1562.     Microformats.parser.newMicroformat(this, node, "tag");
  1563.   }
  1564. }
  1565. tag.prototype.toString = function() {
  1566. //  if (!this.tag) {
  1567. //    return this.text;
  1568. //  }
  1569.   return this.tag;
  1570. }
  1571.  
  1572. var tag_definition = {
  1573.   mfVersion: 0.8,
  1574.   mfObject: tag,
  1575.   attributeName: "rel",
  1576.   attributeValues: "tag",
  1577.   properties: {
  1578.     "tag" : {
  1579.       virtual: true,
  1580.       virtualGetter: function(mfnode) {
  1581.         if (mfnode.href) {
  1582.           var ioService = Components.classes["@mozilla.org/network/io-service;1"].
  1583.                                      getService(Components.interfaces.nsIIOService);
  1584.           var uri = ioService.newURI(mfnode.href, null, null);
  1585.           var url_array = uri.path.split("/");
  1586.           for(let i=url_array.length-1; i > 0; i--) {
  1587.             if (url_array[i] !== "") {
  1588.               var tag
  1589.               if (tag = Microformats.tag.validTagName(url_array[i].replace(/\+/g, ' '))) {
  1590.                 try {
  1591.                   return decodeURIComponent(tag);
  1592.                 } catch (ex) {
  1593.                   return unescape(tag);
  1594.                 }
  1595.               }
  1596.             }
  1597.           }
  1598.         }
  1599.         return null;
  1600.       }
  1601.     },
  1602.     "link" : {
  1603.       virtual: true,
  1604.       datatype: "anyURI"
  1605.     },
  1606.     "text" : {
  1607.       virtual: true
  1608.     }
  1609.   },
  1610.   validTagName: function(tag)
  1611.   {
  1612.     var returnTag = tag;
  1613.     if (tag.indexOf('?') != -1) {
  1614.       if (tag.indexOf('?') === 0) {
  1615.         return false;
  1616.       } else {
  1617.         returnTag = tag.substr(0, tag.indexOf('?'));
  1618.       }
  1619.     }
  1620.     if (tag.indexOf('#') != -1) {
  1621.       if (tag.indexOf('#') === 0) {
  1622.         return false;
  1623.       } else {
  1624.         returnTag = tag.substr(0, tag.indexOf('#'));
  1625.       }
  1626.     }
  1627.     if (tag.indexOf('.html') != -1) {
  1628.       if (tag.indexOf('.html') == tag.length - 5) {
  1629.         return false;
  1630.       }
  1631.     }
  1632.     return returnTag;
  1633.   },
  1634.   validate: function(node, error) {
  1635.     var tag = Microformats.parser.getMicroformatProperty(node, "tag", "tag");
  1636.     if (!tag) {
  1637.       if (node.href) {
  1638.         var url_array = node.getAttribute("href").split("/");
  1639.         for(let i=url_array.length-1; i > 0; i--) {
  1640.           if (url_array[i] !== "") {
  1641.             if (error) {
  1642.               error.message = "Invalid tag name (" + url_array[i] + ")";
  1643.             }
  1644.             return false;
  1645.           }
  1646.         }
  1647.       } else {
  1648.         if (error) {
  1649.           error.message = "No href specified on tag";
  1650.         }
  1651.         return false;
  1652.       }
  1653.     }
  1654.     return true;
  1655.   }
  1656. };
  1657.  
  1658. Microformats.add("tag", tag_definition);
  1659.